package edu.gatech.cc.liam.geometry.linear;

import java.io.Serializable;
import java.text.DecimalFormat;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Iterator;
import java.util.Map.Entry;

import edu.gatech.cc.liam.core.Globals;
import edu.gatech.cc.liam.core.Pair;

public class NPoint extends Vector implements Comparable<NPoint>, Serializable {

	/**
	 * 
	 */
	private static final long serialVersionUID = -2531116210688950081L;
	private static final DecimalFormat printFormat = new DecimalFormat("0.0####");
	
	public double[] values;
	
	private NPoint() {}
	public NPoint(double ... theValues) {
		this.values = theValues.clone();
	}
	public NPoint(Double[] theValues) {
		this.values = new double[theValues.length];
		for(int i=0; i<theValues.length; i++) {
			values[i] = theValues[i];
		}
	}
	public NPoint(int length) {
		this.values = new double[length];
	}
	
	public NPoint(int length, double fillValue) {
		this.values = new double[length];
		for(int i=0; i<length; i++) {
			values[i] = fillValue;
		}
	}
	public NPoint(float[] theValues) {
		this.values = new double[theValues.length];
		for(int i=0; i<theValues.length; i++) {
			values[i] = theValues[i];
		}
	}
	public NPoint(ArrayList<Double> theValues) {
		this.values = new double[theValues.size()];
		for(int i=0; i<theValues.size(); i++) {
			values[i] = theValues.get(i);
		}
	}
	public NPoint(NPoint toClone) {
		this.values = toClone.values.clone();
	}
	
	@Override
	public boolean equals(Object arg0) {
		if (arg0 == null || !getClass().equals(arg0.getClass())) {
	        return false;
	     } else {
	 		return Arrays.equals(values, ((NPoint)arg0).values);
	     }
	}

	@Override
	public boolean softEquals(Vector anotherPoint, double tolerance) {
		if (anotherPoint == null) {
	        return false;
	     } else {
			for(int i=0; i<values.length; i++) {
				if(Math.abs(values[i] - anotherPoint.get(i)) > tolerance)
					return false;
			}
	     }
		return true;
	}

	@Override
	public int hashCode() {
		return Arrays.hashCode(values);
	}

	@Override
	public NPoint clone() {
		return new NPoint(this.values);
	}

	@Override
	public int compareTo(NPoint o) {
		for(int i=0; i<values.length; i++) {
			int positionCompare = Double.valueOf(values[i]).compareTo(o.values[i]);
			if(positionCompare != 0)
				return positionCompare;
		}
		return 0;
	}
	
	@Override
	public String toString() {
		String rStr = "(" + printFormat.format(this.values[0]);
		for(int i=1; i<values.length; i++) {
			rStr += "," + printFormat.format(this.values[i]);
		}
		rStr += ")";
		return rStr;
	}

	public String toSparseString() {
		String rStr = "(";
		for(int i=0; i<values.length; i++) {
			if(Math.abs(this.values[i]) > Globals.TOLERANCE) {
				if(rStr.length() > 1)
					rStr += ", ";
				rStr += "[" + i + "]="+ printFormat.format(this.values[i]);
			}
		}
		rStr += ")";
		return rStr;
	}
	
	public void add(NPoint pointToAdd) {
		assert pointToAdd.values.length == this.values.length;
		for(int i=0; i<values.length; i++) {
			values[i] += pointToAdd.values[i];
		}
	}
	@Override
	public void add(int index, double value) {
		values[index] += value;
	}
	public void subtract(NPoint pointToSubstract) {
		assert pointToSubstract.values.length == this.values.length;
		for(int i=0; i<values.length; i++) {
			values[i] -= pointToSubstract.values[i];
		}
	}
	
	public void pointMult(NPoint pointToMultiply) {
		assert pointToMultiply.values.length == this.values.length;
		for(int i=0; i<values.length; i++) {
			values[i] *= pointToMultiply.values[i];
		}
	}
	
	public void scale(Double scaleFactor) {
		for(int i=0; i<values.length; i++) {
			values[i] *= scaleFactor;
		}
	}
	
	public double l1norm() {
		double sum = 0;
		for(int i=0; i<values.length; i++) {
			sum += values[i];
		}
		return sum;
	}
	
	public double sqDistance(NPoint pt) {
		assert pt.values.length == this.values.length;
		double sumSqares = 0.0;
		for(int i=0; i<pt.values.length; i++) {
			double d = this.values[i] - pt.values[i];
			sumSqares += d*d;
		}
		return sumSqares;
	}
	public double distance(NPoint pt) {
		return Math.sqrt(this.sqDistance(pt));
	}
	
	@Override
	public int getNumEntries() {
		return this.values.length;
	}
	public double dotProduct(NPoint otherVector) {
		return dotProduct(otherVector.values);
	}
	@Override
	public double dotProduct(Vector otherVector) {
		if(NPoint.class.isInstance(otherVector)) {
			return dotProduct((NPoint) otherVector);
		} else {
			assert otherVector.getClass() == SparsePoint.class;
			return otherVector.dotProduct(this);
		}
	}
	
	public double dotProduct(double[] otherVector) {
		assert otherVector.length == this.values.length;
		double sum = 0;
		for(int i=0; i<values.length; i++) {
			sum += this.values[i] * otherVector[i];
		}
		return sum;
	}
	
	public double angle2D(NPoint center) {
		assert this.values.length == 2;
		NPoint translated = NPoint.sum(this, center);
		return Math.atan2(translated.values[1], translated.values[0]);
	}
	
	public NPoint dropDimension(int dimToDrop) {
		double[] newVals = Arrays.copyOf(values, values.length-1);
		for(int i=dimToDrop; i<values.length-1; i++) {
			newVals[i]  = values[i+1];
		}
		return new NPoint(newVals);
	}
	
	public void normalize() {
		double sqsum = 0;
		for(int i=0; i<values.length; i++) {
			sqsum += values[i]*values[i];
		}
		for(int i=0; i<values.length; i++) {
			values[i] /= Math.sqrt(sqsum);
		}
	}
	public int dimensions() {
		return this.values.length;
	}
	
	public static NPoint parseSpaceSeperatedNPoint(String lineOfNumbers) {
		String[] theCords = lineOfNumbers.split("\\s+");
		ArrayList<Double> newPoint = new ArrayList<Double>();
		for(int i=0; i<theCords.length; i++) {
			if(theCords[i].equals("")) 
				continue;
			newPoint.add(Double.parseDouble(theCords[i]));
		}
		return new NPoint(newPoint);
	}
	
	/**
	 * This makes a random NPoint with each value being randomly between minVal and maxVal
	 */
	public static NPoint makeLinearRandom(int dimensions, double minVal, double maxVal) {
		double[] minVals = new double[dimensions];
		double[] maxVals = new double[dimensions];
		Arrays.fill(minVals, minVal);
		Arrays.fill(maxVals, maxVal);
		return makeLinearRandom(dimensions, minVals, maxVals);
	}
	
	/**
	 * This makes a random NPoint with each value being randomly between minVal and maxVal
	 * @param dimensions - the number of dimensions of the returned point
	 * @param minVals - a set of minimum values, one for each dimension
	 * @param maxVals - a set of maximum values, one for each dimension
	 * @return a point with each coordinate being assigned IID between minVal and MaxVal
	 */
	public static NPoint makeLinearRandom(int dimensions, double[] minVals, double[] maxVals) {
		double[] newPoint = new double[dimensions];
		for(int i=0; i<dimensions; i++) {
			double e = Globals.rand.nextDouble();
			newPoint[i] = minVals[i] * e + maxVals[i] * (1.0-e);
		}
		return new NPoint(newPoint);
	}
	/**
	 * @param dimensions - the number of dimensions of the returned point
	 * @return a point sampled uniformly from the unit sphere
	 */
	public static NPoint makePolarRandom(int dimensions) {
		double[] newPoint = new double[dimensions];
		double sqSum = 0;
		for(int i=0; i<dimensions; i++) {
			newPoint[i] = Globals.rand.nextGaussian();
			sqSum += newPoint[i] * newPoint[i];
		}
		sqSum = Math.sqrt(sqSum);
		for(int i=0; i<dimensions; i++) {
			newPoint[i] /= sqSum;
		}
		return new NPoint(newPoint);
	}
	public static NPoint scalarMult(NPoint nPoint, Double scaleFactor) {
		NPoint r = nPoint.clone();
		r.scale(scaleFactor);
		return r;
	}
	public static NPoint sum(NPoint p1, NPoint p2) {
		Double[] newP = new Double[p1.values.length];
		for(int i=0; i<p1.values.length; i++) {
			newP[i] = p1.values[i] + p2.values[i];
		}
		return new NPoint(newP);
	}
	public static NPoint pointMult(NPoint p1, NPoint p2) {
		Double[] newP = new Double[p1.values.length];
		for(int i=0; i<p1.values.length; i++) {
			newP[i] = p1.values[i] * p2.values[i];
		}
		return new NPoint(newP);
	}
	public static NPoint pointDiv(NPoint p1, NPoint p2) {
		Double[] newP = new Double[p1.values.length];
		for(int i=0; i<p1.values.length; i++) {
			newP[i] = p1.values[i] / p2.values[i];
		}
		return new NPoint(newP);
	}
	public static double dotProd(double[] p1, double[] p2) {
		NPoint wrapper = new NPoint();
		wrapper.values = p1;
		return wrapper.dotProduct(p2);
	}
	public static NPoint resize(NPoint p1, int newLength) {
		return new NPoint(Arrays.copyOf(p1.values, newLength));
	}
	@Override
	public double get(int i) {
		return values[i];
	}
	@Override
	public void set(int i, double value) {
		values[i] = value;
	}
	@Override
	public Iterator<Entry<Integer, Double>> iterator() {
		return new Iterator<Entry<Integer, Double>>() {
			int pos = 0;
			@Override
			public boolean hasNext() {
				return pos < values.length;
			}
			@Override
			public Entry<Integer, Double> next() {
				Pair<Integer, Double> r = new Pair<Integer, Double>(pos, values[pos]);
				pos++;
				return r;
			}
			@Override
			public void remove() {
				throw new UnsupportedOperationException();
			}
		};
	}

}
